2026_laurent_garnier_crispy_bacon.py

#

SPDX-FileCopyrightText: 2026 Pierre-Elie Mahussier & Rafael Rodrigues Braguês SPDX-FileCopyrightText: 2026 AlICe laboratory https://alicelab.be

SPDX-License-Identifier: GPL-3.0-or-later

#

Blender 5.0.1 - hash : a3bd93c5b259

import math
import os
import random

import bmesh
import bpy
from mathutils import Vector
#

PARAMÈTRES #

BASS_NAMES = [
    "Bass.001",
    "Bass.002",
    "Bass.003",
    "Bass.004",  # Liste des basses
    "Bass.005",
    "Bass.006",
    "Bass.007",
    "Bass.008",
]

ROW_GAP = 0.5  # l’espace entre les basses placées côte à côte
CUBE_X = 0.0  # position X du cube principal
CUBE_Y = 0.0  # position Y du cube principal
DRUMS_Y_OFFSET = 20  # décalage des drums par rapport au cube (pas celui sur les face du cube mais tout les drums du morceau)
BOOLEAN_OFFSET = 2.5  # dimension du cutter boolean
BOOLEAN_SOLVER = "FLOAT"  # type de solveur boolean

DRUM_MARGIN = 0.1  # petite marge de sécurité entre les drums placés
DRUM_PAD_FACTOR = 0.07  # facteur de rembourrage qui empêche les objets d’être placés trop près des bords.
DRUM_BUFFER = 1.10  # C’est un petit buffer qui agrandit légèrement les dimensions utilisées pour vérifier les collisions
HIHAT_GAP = 1.4  # espace entre les deux parties du hi-hat

MAX_BOOLEAN_BOXES = 12  # nombre maximum de booleans appliqués pendant une exécution
CUTTER_SHIFT_FACTOR = 0.350  # facteur qui contrôle le déplacement aléatoire des cutters pour que les coupes ne soient pas toujours identiques
#

NETTOYAGE #

#
def reset_object(_obj):
    bpy.ops.object.select_all(action="DESELECT")
    _obj.select_set(True)
    bpy.context.view_layer.objects.active = _obj
    _obj.location = Vector((0.0, 0.0, 0.0))
    _obj.rotation_euler = (0.0, 0.0, 0.0)
    _obj.scale = Vector((1.0, 1.0, 1.332))
    bpy.context.view_layer.update()
    bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY", center="BOUNDS")
    _obj.location = Vector((0.0, 0.0, 0.0))
    bpy.context.view_layer.update()
#

CALCUL DES DIMENSIONS #

Retourne les coordonnées minimales et maximales de l’objet

#
def get_extents(_obj):
    bpy.context.view_layer.update()
    _corners = [_obj.matrix_world @ Vector(c) for c in _obj.bound_box]
    _xs = [v.x for v in _corners]
    _ys = [v.y for v in _corners]
    _zs = [v.z for v in _corners]
    return min(_xs), max(_xs), min(_ys), max(_ys), min(_zs), max(_zs)
#

DUPLICATION ET SUPPRESSION TEMP #

Duplication des objets

#
def duplicate_object(_obj, _new_name=None):
    _dup = _obj.copy()
    _dup.data = _obj.data.copy()
    _dup.animation_data_clear()

    if _new_name:
        _dup.name = _new_name
        _dup.data.name = _new_name + "_mesh"

    bpy.context.collection.objects.link(_dup)
    return _dup
#

Supprime les objets temporaires du cube precedent

def delete_temp_objects():
    _to_delete = [
        _obj
        for _obj in bpy.data.objects
        if _obj.name.endswith("_TEMP")
        or _obj.name.startswith("BOOL_")
        or _obj.name.startswith("MESURE_")
    ]

    for _obj in _to_delete:
        bpy.data.objects.remove(_obj, do_unlink=True)
#

FORMES DE DRUMS #

Fonction de la formes pour creation des drums

#
def beat_box(_pos, _size, _name):
    _outer = _size
    _inner = (_size[0] * 0.7, _size[1] * 2, _size[2] * 0.8)

    bpy.ops.mesh.primitive_cube_add(size=1, location=_pos, scale=_outer)
    outer = bpy.context.object
    outer.name = _name
    outer.data.name = _name + "_mesh"

    bpy.ops.mesh.primitive_cube_add(size=1, location=_pos, scale=_inner)
    inner = bpy.context.object
    inner.name = _name + "_cutter"
    inner.data.name = _name + "_cutter_mesh"

    bool_mod = outer.modifiers.new(name="Boolean_cut", type="BOOLEAN")
    bool_mod.operation = "DIFFERENCE"
    bool_mod.object = inner
    bool_mod.solver = "FLOAT"

    bpy.context.view_layer.objects.active = outer
    bpy.ops.object.modifier_apply(modifier="Boolean_cut")
    bpy.data.objects.remove(inner, do_unlink=True)

    outer.rotation_euler[0] = math.radians(90)
    return outer
#
def beat_triangle(_pos, _size, _name):
    mesh = bpy.data.meshes.new(_name + "_mesh")
    obj = bpy.data.objects.new(_name, mesh)
    bpy.context.collection.objects.link(obj)

    bm = bmesh.new()
    x, y, z = _size

    v1 = bm.verts.new((-x, 0, -z))
    v2 = bm.verts.new((x, 0, -z))
    v3 = bm.verts.new((0, 0, z))

    v4 = bm.verts.new((-x, y, -z))
    v5 = bm.verts.new((x, y, -z))
    v6 = bm.verts.new((0, y, z))

    bm.faces.new((v1, v2, v3))
    bm.faces.new((v4, v5, v6))
    bm.faces.new((v1, v2, v5, v4))
    bm.faces.new((v2, v3, v6, v5))
    bm.faces.new((v3, v1, v4, v6))

    bm.to_mesh(mesh)
    bm.free()

    obj.location = _pos
    obj.rotation_mode = "XYZ"
    obj.rotation_euler = (0.0, 0.0, 0.0)
    return obj
#
def beat_clap(_pos, _size, _name):
    bpy.ops.mesh.primitive_cube_add(
        size=1,
        location=_pos,
        scale=(_size[0] * 0.5, _size[1] * 1.90, _size[2] * 0.20),
    )
    bpy.context.object.name = _name
    bpy.context.object.data.name = _name + "_mesh"
    bpy.context.object.rotation_euler = (0.0, 0.0, 0.0)
    return bpy.context.object
#
def beat_snare(_pos, _size, _name):
    arm_thickness = _size[0] * 0.35
    arm_length = _size[1] * 1.6
    depth = _size[2] * 0.22

    bpy.ops.mesh.primitive_cube_add(
        size=0.8,
        location=_pos,
        scale=(arm_length, arm_thickness, depth),
    )
    rect_1 = bpy.context.object
    rect_1.name = _name + "_part1"
    rect_1.data.name = _name + "_part1_mesh"

    bpy.ops.mesh.primitive_cube_add(
        size=0.8,
        location=_pos,
        scale=(arm_thickness, arm_length, depth),
    )
    rect_2 = bpy.context.object
    rect_2.name = _name + "_part2"
    rect_2.data.name = _name + "_part2_mesh"

    bpy.ops.object.select_all(action="DESELECT")
    rect_1.select_set(True)
    rect_2.select_set(True)
    bpy.context.view_layer.objects.active = rect_1
    bpy.ops.object.join()

    snare = bpy.context.object
    snare.name = _name
    snare.data.name = _name + "_mesh"
    return snare
#
def beat_hihat(_pos, _size, _name):
    bpy.ops.mesh.primitive_cube_add(size=2, location=_pos, scale=_size)
    r1 = bpy.context.object
    r1.name = _name
    r1.data.name = _name + "_mesh"

    bpy.ops.mesh.primitive_cube_add(size=2, location=_pos, scale=_size)
    r2 = bpy.context.object
    r2.name = _name + ".001"
    r2.data.name = _name + ".001_mesh"

    return [r1, r2]
#
def beat_triangle_box(_pos, _size, _name):
    mesh = bpy.data.meshes.new(_name + "_mesh")
    obj = bpy.data.objects.new(_name, mesh)
    bpy.context.collection.objects.link(obj)

    bm = bmesh.new()
    x, y, z = _size

    v1 = bm.verts.new((-x, 0, -z))
    v2 = bm.verts.new((x, 0, -z))
    v3 = bm.verts.new((0, 0, z))
    v4 = bm.verts.new((-x, y, -z))
    v5 = bm.verts.new((x, y, -z))
    v6 = bm.verts.new((0, y, z))

    bm.faces.new((v1, v2, v3))
    bm.faces.new((v4, v5, v6))
    bm.faces.new((v1, v2, v5, v4))
    bm.faces.new((v2, v3, v6, v5))
    bm.faces.new((v3, v1, v4, v6))

    bm.to_mesh(mesh)
    bm.free()

    obj.location = _pos
    return obj
#
def beat_losange(_pos, _size, _name):
    mesh = bpy.data.meshes.new(_name + "_mesh")
    obj = bpy.data.objects.new(_name, mesh)
    bpy.context.collection.objects.link(obj)

    bm = bmesh.new()
    x, y, z = _size

    v1 = bm.verts.new((0, 0, z))
    v2 = bm.verts.new((x, 0, 0))
    v3 = bm.verts.new((0, 0, -z))
    v4 = bm.verts.new((-x, 0, 0))

    v5 = bm.verts.new((0, y, z))
    v6 = bm.verts.new((x, y, 0))
    v7 = bm.verts.new((0, y, -z))
    v8 = bm.verts.new((-x, y, 0))

    bm.faces.new((v1, v2, v3, v4))
    bm.faces.new((v5, v6, v7, v8))
    bm.faces.new((v1, v2, v6, v5))
    bm.faces.new((v2, v3, v7, v6))
    bm.faces.new((v3, v4, v8, v7))
    bm.faces.new((v4, v1, v5, v8))

    bm.to_mesh(mesh)
    bm.free()

    obj.location = _pos
    return obj
#

OUTILS DE POSITIONNEMENT #

Calcule les coordonnées de placement des éléments des drums de tout le morceau derriere le cube

PART_WIDTH = 32
#
def ox(x, part):
    return x + part * PART_WIDTH
#
def oy(y):
    return y + DRUMS_Y_OFFSET
#

RÉCUPÉRER LES BASSES ORIGINALES #

Récupère les objets de basse d’origine

_original_basses = []
for _name in BASS_NAMES:
    _obj = bpy.data.objects.get(_name)
    if _obj and _obj.type == "MESH":
        _original_basses.append(_obj)
#

SUPPRIMER LES ANCIENS OBJETS TEMPORAIRES #

Supprime les objets temporaires du cube precedent

delete_temp_objects()
#

RÉINITIALISER LES BASSES ORIGINALES #

Réinitialise et remet en cache les basses originales

for _obj in _original_basses:
    _obj.hide_viewport = False
    _obj.hide_render = False
    reset_object(_obj)
    _obj.hide_viewport = True
    _obj.hide_render = True
#

DUPLIQUER TOUTES LES BASSES EN _TEMP #

Duplique les basses originales en objets temporaires pour la creation du nouveau cube

_all_basses = []
for _orig in _original_basses:
    _dup = duplicate_object(_orig, _orig.name + "_TEMP")
    reset_object(_dup)
    _dup.hide_viewport = True
    _dup.hide_render = True
    _all_basses.append(_dup)
#

MÉLANGER TOUTES LES BASSES ET CHOISIR 3 POUR LE CUBE #

Mélange les basses et sélectionne 3 objets pour le cube

random.shuffle(_all_basses)

_selected = _all_basses[:3]
_not_selected = _all_basses[3:]
#

CONSTRUCTION DU CUBE #

Empile les basses sélectionnés pour former le cube

_current_z = 0.0
_cube_x_min = None
_cube_x_max = None
_cube_y_min = None
_cube_y_max = None
_cube_z_min = 0.0
_cube_z_max = None

for _obj in _selected:
    _obj.hide_viewport = False
    _obj.hide_render = False

    _rot_deg = random.choice([0, 90, 180, 270])
    _obj.rotation_euler = (0.0, 0.0, math.radians(_rot_deg))
    _obj.location = Vector((0.0, 0.0, 0.0))

    bpy.context.view_layer.update()
    _x_min, _x_max, _y_min, _y_max, _z_min, _z_max = get_extents(_obj)

    _obj.location.x = CUBE_X - (_x_min + _x_max) / 2
    _obj.location.y = CUBE_Y - (_y_min + _y_max) / 2
    _obj.location.z = _current_z - _z_min

    bpy.context.view_layer.update()
    _xn, _xx, _yn, _yx, _zn, _zx = get_extents(_obj)

    if _cube_x_min is None or _xn < _cube_x_min:
        _cube_x_min = _xn
    if _cube_x_max is None or _xx > _cube_x_max:
        _cube_x_max = _xx
    if _cube_y_min is None or _yn < _cube_y_min:
        _cube_y_min = _yn
    if _cube_y_max is None or _yx > _cube_y_max:
        _cube_y_max = _yx
    if _cube_z_max is None or _zx > _cube_z_max:
        _cube_z_max = _zx

    _current_z = _zx

_cube_width = _cube_x_max - _cube_x_min
_cube_height = _cube_z_max - _cube_z_min
DRUM_SCALE_FACTOR = _cube_width / 16
#

PLACER LES BASSES RESTANTES À CÔTÉ #

Place les basses non seléectioner à côté du cube

_current_x = _cube_x_max + ROW_GAP * 4

for _obj in _not_selected:
    _obj.hide_viewport = False
    _obj.hide_render = False

    _rot_deg = random.choice([0, 90, 180, 270])
    _obj.rotation_euler = (0.0, 0.0, math.radians(_rot_deg))
    _obj.location = Vector((0.0, 0.0, 0.0))

    bpy.context.view_layer.update()
    _x_min, _x_max, _y_min, _y_max, _z_min, _z_max = get_extents(_obj)

    _obj.location.x = _current_x - _x_min
    _obj.location.y = CUBE_Y - (_y_min + _y_max) / 2
    _obj.location.z = -_z_min

    bpy.context.view_layer.update()
    _current_x += (_x_max - _x_min) + ROW_GAP
#

SUPPRIMER LES ANCIENNES MESURES #

Supprime les anciens drums du cube precedent

for _obj in [o for o in bpy.data.objects if o.name.startswith("MESURE_")]:
    bpy.data.objects.remove(_obj, do_unlink=True)

S = DRUM_SCALE_FACTOR
#

GÉNÉRER UNE MESURE #

Génère les drums du morceau complet derriere le cube

#
def generate_mesure(_part, _prefix):
    _objs = []

    if _part == 0:
        for i in range(1, 5):
            _objs.append(beat_box((i * 2, 0, 0), (S, S, S), f"{_prefix}_Kick{i}"))
        _objs.append(
            beat_triangle(
                (4 * 2, 0, 0), (S * 0.45, S * 0.08, S * 0.45), f"{_prefix}_FX4"
            ),
        )

    elif _part == 1:
        for i in range(1, 5):
            _objs.append(beat_box((i * 2, 0, 0), (S, S, S), f"{_prefix}_Kick{i}"))
            _objs.append(
                beat_snare((i * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Snare{i}")
            )
        _objs.append(
            beat_triangle(
                (4 * 2, 0, 0), (S * 0.45, S * 0.08, S * 0.45), f"{_prefix}_FX4"
            ),
        )

    elif _part == 2:
        for i in range(1, 5):
            _objs.append(
                beat_snare((i * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Snare{i}")
            )
        _objs.append(
            beat_triangle(
                (4 * 2, 0, 0), (S * 0.45, S * 0.08, S * 0.45), f"{_prefix}_FX4"
            ),
        )

    elif _part == 3:
        for i in range(1, 5):
            _objs.append(beat_box((i * 2, 0, 0), (S, S, S), f"{_prefix}_Kick{i}"))
            _objs.extend(
                beat_hihat(
                    (i * 2, 0, 0), (S * 0.15, S, S * 0.5), f"{_prefix}_Hihat{i}"
                ),
            )
        _objs.append(beat_snare((3 * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Snare3"))
        _objs.append(
            beat_clap((4 * 2, 0, 0), (S * 0.5, S, S * 0.5), f"{_prefix}_Clap4")
        )
        _objs.append(
            beat_triangle(
                (4 * 2, 0, 0), (S * 0.45, S * 0.08, S * 0.45), f"{_prefix}_FX4"
            ),
        )

    elif _part == 4:
        for i in range(1, 5):
            _objs.extend(
                beat_hihat(
                    (i * 2, 0, 0), (S * 0.15, S, S * 0.5), f"{_prefix}_Hihat{i}"
                ),
            )
        _objs.append(
            beat_clap((3 * 2, 0, 0), (S * 0.5, S, S * 0.5), f"{_prefix}_Clap3")
        )
        _objs.append(beat_losange((4 * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Crash4"))
        _objs.append(
            beat_triangle(
                (4 * 2, 0, 0), (S * 0.45, S * 0.08, S * 0.45), f"{_prefix}_FX4"
            ),
        )

    elif _part == 5:
        for i in range(1, 5):
            _objs.append(beat_box((i * 2, 0, 0), (S, S, S), f"{_prefix}_Kick{i}"))
            _objs.extend(
                beat_hihat(
                    (i * 2, 0, 0), (S * 0.15, S, S * 0.5), f"{_prefix}_Hihat{i}"
                ),
            )
            _objs.append(
                beat_snare((i * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Snare{i}")
            )
        _objs.append(
            beat_clap((3 * 2, 0, 0), (S * 0.5, S, S * 0.5), f"{_prefix}_Clap3")
        )
        _objs.append(beat_losange((4 * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Crash4"))
        _objs.append(
            beat_triangle(
                (4 * 2, 0, 0), (S * 0.45, S * 0.08, S * 0.45), f"{_prefix}_FX4"
            ),
        )

    elif _part == 6:
        for i in range(1, 5):
            _objs.append(beat_box((i * 2, 0, 0), (S, S, S), f"{_prefix}_Kick{i}"))
            _objs.extend(
                beat_hihat(
                    (i * 2, 0, 0), (S * 0.15, S, S * 0.5), f"{_prefix}_Hihat{i}"
                ),
            )
        _objs.append(beat_snare((2 * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Snare2"))
        _objs.append(
            beat_clap((3 * 2, 0, 0), (S * 0.5, S, S * 0.5), f"{_prefix}_Clap3")
        )
        _objs.append(beat_losange((4 * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Crash4"))
        _objs.append(
            beat_triangle(
                (4 * 2, 0, 0), (S * 0.45, S * 0.08, S * 0.45), f"{_prefix}_FX4"
            ),
        )

    elif _part == 7:
        for i in range(1, 5):
            _objs.append(
                beat_snare((i * 2, 0, 0), (S, S, S * 0.5), f"{_prefix}_Snare{i}")
            )

    return _objs
#

OUTILS DE COLLISION 2D SUR UNE FACE #

détection de collision sur une face

#
def _face_overlap(_a, _b, _margin=0.0):
    return not (
        _a[1] + _margin < _b[0]
        or _a[0] - _margin > _b[1]
        or _a[3] + _margin < _b[2]
        or _a[2] - _margin > _b[3]
    )
#
def _world_hu_hv_front(_obj):
    _ex = get_extents(_obj)
    return ((_ex[1] - _ex[0]) / 2) * DRUM_BUFFER, ((_ex[5] - _ex[4]) / 2) * DRUM_BUFFER
#
def _world_hu_hv_side(_obj):
    _ex = get_extents(_obj)
    return ((_ex[3] - _ex[2]) / 2) * DRUM_BUFFER, ((_ex[5] - _ex[4]) / 2) * DRUM_BUFFER
#

PLACER LE PARTENAIRE DU HI-HAT SI NÉCESSAIRE #

Place le second élément du hi-hat si nécessaire / deuxieme rectangle pour former ca forme de deux rectangles

#
def _place_hihat_partner(
    _obj,
    _partner,
    _up,
    _vp,
    _placed_2d,
    _face,
    _gap,
    _length,
    _cymin,
    _cymax,
    _cxmin,
    _cxmax,
):
    bpy.context.view_layer.update()
    _ex_p = get_extents(_partner)

    if _face in ("front", "right"):
        _pw = _ex_p[1] - _ex_p[0]
        _sg = _pw * HIHAT_GAP
        _partner.location.x = _up + _sg
        _partner.location.z = _vp
        _partner.location.y = (
            _cymin - _gap + _length / 2
            if _face == "front"
            else _cymax + _gap - _length / 2
        )

        bpy.context.view_layer.update()
        _ex_p2 = get_extents(_partner)
        _placed_2d.append((_ex_p2[0], _ex_p2[1], _ex_p2[4], _ex_p2[5]))

    else:
        _pw = _ex_p[3] - _ex_p[2]
        _sg = _pw * HIHAT_GAP
        _partner.location.y = _up + _sg
        _partner.location.z = _vp
        _partner.location.x = (
            _cxmin - _gap + _length / 2
            if _face == "left"
            else _cxmax + _gap - _length / 2
        )

        bpy.context.view_layer.update()
        _ex_p2 = get_extents(_partner)
        _placed_2d.append((_ex_p2[2], _ex_p2[3], _ex_p2[4], _ex_p2[5]))
#

PLACER UNE MESURE SUR UNE FACE DU CUBE #

Positionne les objets d’une mesure sur une face du cube (front,left,back,right / faces)

#
def place_takt_on_face(_objects, _face, _cxmin, _cxmax, _cymin, _cymax, _czmin, _czmax):
    _depth = _cymax - _cymin
    _gap = _depth * 0.02
    _margin = DRUM_MARGIN
    _pad = _cube_width * DRUM_PAD_FACTOR
    _placed_2d = []
    _processed = set()

    for _obj in _objects:
        if _obj.name in _processed:
            continue

        _is_crash = "Crash" in _obj.name
        _is_fx = "FX" in _obj.name
        _length = random.uniform(_depth * 0.25, _depth * 0.95)

        _is_hihat_pair = "Hihat" in _obj.name and not _obj.name.endswith(".001")
        _partner = None

        if _is_hihat_pair:
            _partner = bpy.data.objects.get(_obj.name + ".001")
            if _partner:
                _processed.add(_partner.name)
                _partner.rotation_euler = (0.0, 0.0, 0.0)

        if _face == "front":
            if "Kick" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, 0.0)
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_hihat_pair:
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)
                if _partner:
                    _partner.scale.y = _length / max(_partner.dimensions.y, 0.0001)

            elif "Hihat" in _obj.name:
                _processed.add(_obj.name)
                continue

            elif "Clap" in _obj.name:
                _obj.rotation_euler = (0.0, math.radians(90), math.radians(90))
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif "Snare" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, 0.0)
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_fx:
                _obj.rotation_mode = "XYZ"
                _obj.rotation_euler = (0.0, 0.0, math.radians(0))
                bpy.context.view_layer.update()
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            else:
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            bpy.context.view_layer.update()
            _hu, _hv = _world_hu_hv_front(_obj)

            for _ in range(150):
                _up = random.uniform(_cxmin + _hu + _pad, _cxmax - _hu - _pad)
                _vp = random.uniform(_czmin + _hv + _pad, _czmax - _hv - _pad)
                _b = (_up - _hu, _up + _hu, _vp - _hv, _vp + _hv)
                if not any(_face_overlap(_b, _x, _margin) for _x in _placed_2d):
                    _placed_2d.append(_b)
                    break

            _obj.location.x = _up
            _obj.location.z = _vp

            if _is_crash:
                _obj.location.y = _cymin - _gap
            elif _is_fx:
                _obj.location.y = _cymin - 0.02
            else:
                _obj.location.y = _cymin - _gap + _length / 2

            if _is_hihat_pair and _partner:
                _place_hihat_partner(
                    _obj,
                    _partner,
                    _up,
                    _vp,
                    _placed_2d,
                    "front",
                    _gap,
                    _length,
                    _cymin,
                    _cymax,
                    _cxmin,
                    _cxmax,
                )

        elif _face == "left":
            if "Kick" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, math.radians(270))
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_hihat_pair:
                _obj.rotation_euler[2] = math.radians(270)
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)
                if _partner:
                    _partner.rotation_euler[2] = math.radians(270)
                    _partner.scale.y = _length / max(_partner.dimensions.y, 0.0001)

            elif "Hihat" in _obj.name:
                _processed.add(_obj.name)
                continue

            elif "Clap" in _obj.name:
                _obj.rotation_euler = (
                    math.radians(90),
                    math.radians(90),
                    math.radians(90),
                )
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif "Snare" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, math.radians(270))
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_fx:
                _obj.rotation_mode = "XYZ"
                _obj.rotation_euler = (0.0, 0.0, math.radians(270))
                bpy.context.view_layer.update()
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            else:
                _obj.rotation_euler[2] = math.radians(270)
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            bpy.context.view_layer.update()
            _hu, _hv = _world_hu_hv_side(_obj)

            for _ in range(150):
                _up = random.uniform(_cymin + _hu + _pad, _cymax - _hu - _pad)
                _vp = random.uniform(_czmin + _hv + _pad, _czmax - _hv - _pad)
                _b = (_up - _hu, _up + _hu, _vp - _hv, _vp + _hv)
                if not any(_face_overlap(_b, _x, _margin) for _x in _placed_2d):
                    _placed_2d.append(_b)
                    break

            _obj.location.y = _up
            _obj.location.z = _vp

            if _is_crash:
                _obj.location.x = _cxmin - _gap
            elif _is_fx:
                _obj.location.x = _cxmin - 0.02
            else:
                _obj.location.x = _cxmin - _gap + _length / 2

            if _is_hihat_pair and _partner:
                _place_hihat_partner(
                    _obj,
                    _partner,
                    _up,
                    _vp,
                    _placed_2d,
                    "left",
                    _gap,
                    _length,
                    _cymin,
                    _cymax,
                    _cxmin,
                    _cxmax,
                )

        elif _face == "right":
            if "Kick" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, math.radians(180))
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_hihat_pair:
                _obj.rotation_euler[2] = math.radians(180)
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)
                if _partner:
                    _partner.rotation_euler[2] = math.radians(180)
                    _partner.scale.y = _length / max(_partner.dimensions.y, 0.0001)

            elif "Hihat" in _obj.name:
                _processed.add(_obj.name)
                continue

            elif "Clap" in _obj.name:
                _obj.rotation_euler = (0.0, math.radians(90), 0.0)
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif "Snare" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, math.radians(180))
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_fx:
                _obj.rotation_mode = "XYZ"
                _obj.rotation_euler = (0.0, 0.0, math.radians(180))
                bpy.context.view_layer.update()
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            else:
                _obj.rotation_euler[2] = math.radians(180)
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            bpy.context.view_layer.update()
            _hu, _hv = _world_hu_hv_front(_obj)

            for _ in range(150):
                _up = random.uniform(_cxmin + _hu + _pad, _cxmax - _hu - _pad)
                _vp = random.uniform(_czmin + _hv + _pad, _czmax - _hv - _pad)
                _b = (_up - _hu, _up + _hu, _vp - _hv, _vp + _hv)
                if not any(_face_overlap(_b, _x, _margin) for _x in _placed_2d):
                    _placed_2d.append(_b)
                    break

            _obj.location.x = _up
            _obj.location.z = _vp

            if _is_crash:
                _obj.location.y = _cymax + _gap
            elif _is_fx:
                _obj.location.y = _cymax + 0.02
            else:
                _obj.location.y = _cymax + _gap - _length / 2

            if _is_hihat_pair and _partner:
                _place_hihat_partner(
                    _obj,
                    _partner,
                    _up,
                    _vp,
                    _placed_2d,
                    "right",
                    _gap,
                    _length,
                    _cymin,
                    _cymax,
                    _cxmin,
                    _cxmax,
                )

        elif _face == "back":
            if "Kick" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, math.radians(90))
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_hihat_pair:
                _obj.rotation_euler[2] = math.radians(90)
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)
                if _partner:
                    _partner.rotation_euler[2] = math.radians(90)
                    _partner.scale.y = _length / max(_partner.dimensions.y, 0.0001)

            elif "Hihat" in _obj.name:
                _processed.add(_obj.name)
                continue

            elif "Clap" in _obj.name:
                _obj.rotation_euler = (0.0, math.radians(90), 0.0)
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif "Snare" in _obj.name:
                _obj.rotation_euler = (math.radians(90), 0.0, math.radians(90))
                bpy.context.view_layer.update()
                _obj.scale.z = _length / max(_obj.dimensions.z, 0.0001)

            elif _is_fx:
                _obj.rotation_mode = "XYZ"
                _obj.rotation_euler = (0.0, 0.0, math.radians(90))
                bpy.context.view_layer.update()
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            else:
                _obj.rotation_euler[2] = math.radians(90)
                _obj.scale.y = _length / max(_obj.dimensions.y, 0.0001)

            bpy.context.view_layer.update()
            _hu, _hv = _world_hu_hv_side(_obj)

            for _ in range(150):
                _up = random.uniform(_cymin + _hu + _pad, _cymax - _hu - _pad)
                _vp = random.uniform(_czmin + _hv + _pad, _czmax - _hv - _pad)
                _b = (_up - _hu, _up + _hu, _vp - _hv, _vp + _hv)
                if not any(_face_overlap(_b, _x, _margin) for _x in _placed_2d):
                    _placed_2d.append(_b)
                    break

            _obj.location.y = _up
            _obj.location.z = _vp

            if _is_crash:
                _obj.location.x = _cxmax + _gap
            elif _is_fx:
                _obj.location.x = _cxmax + 0.02
            else:
                _obj.location.x = _cxmax + _gap - _length / 2

            if _is_hihat_pair and _partner:
                _place_hihat_partner(
                    _obj,
                    _partner,
                    _up,
                    _vp,
                    _placed_2d,
                    "back",
                    _gap,
                    _length,
                    _cymin,
                    _cymax,
                    _cxmin,
                    _cxmax,
                )

        bpy.context.view_layer.update()
        _processed.add(_obj.name)
#

GÉNÉRER 4 MESURES SUR 4 FACES DU CUBE #

Génère et place 4 mesures sur les 4 faces du cube

_all_parts = list(range(8))
random.shuffle(_all_parts)

_mesure_part_front = _all_parts[0]
_mesure_part_left = _all_parts[1]
_mesure_part_right = _all_parts[2]
_mesure_part_back = _all_parts[3]

_mesure_objs_front = generate_mesure(
    _mesure_part_front,
    f"MESURE_FRONT_P{_mesure_part_front + 1}",
)
place_takt_on_face(
    _mesure_objs_front,
    "front",
    _cube_x_min,
    _cube_x_max,
    _cube_y_min,
    _cube_y_max,
    _cube_z_min,
    _cube_z_max,
)

_mesure_objs_left = generate_mesure(
    _mesure_part_left, f"MESURE_LEFT_P{_mesure_part_left + 1}"
)
place_takt_on_face(
    _mesure_objs_left,
    "left",
    _cube_x_min,
    _cube_x_max,
    _cube_y_min,
    _cube_y_max,
    _cube_z_min,
    _cube_z_max,
)

_mesure_objs_right = generate_mesure(
    _mesure_part_right,
    f"MESURE_RIGHT_P{_mesure_part_right + 1}",
)
place_takt_on_face(
    _mesure_objs_right,
    "right",
    _cube_x_min,
    _cube_x_max,
    _cube_y_min,
    _cube_y_max,
    _cube_z_min,
    _cube_z_max,
)

_mesure_objs_back = generate_mesure(
    _mesure_part_back, f"MESURE_BACK_P{_mesure_part_back + 1}"
)
place_takt_on_face(
    _mesure_objs_back,
    "back",
    _cube_x_min,
    _cube_x_max,
    _cube_y_min,
    _cube_y_max,
    _cube_z_min,
    _cube_z_max,
)

_mesure_objects = (
    _mesure_objs_front + _mesure_objs_left + _mesure_objs_right + _mesure_objs_back
)
#

ASSIGNER LE MATÉRIAU AUX MESURES #

Assigne un matériau aux drums

_mat_mesure = bpy.data.materials.get("Material.002")
if _mat_mesure:
    for _obj in _mesure_objects:
        if _obj.type == "MESH":
            _obj.data.materials.clear()
            _obj.data.materials.append(_mat_mesure)
#

PLACER LES MESURES DANS UNE COLLECTION #

Regroupe les drums du cube dans une collection

_col_name = "Mesures"

if _col_name in bpy.data.collections:
    bpy.data.collections.remove(bpy.data.collections[_col_name])

_mesure_col = bpy.data.collections.new(_col_name)
bpy.context.scene.collection.children.link(_mesure_col)

for _obj in _mesure_objects:
    for _c in list(_obj.users_collection):
        _c.objects.unlink(_obj)
    _mesure_col.objects.link(_obj)
#

CRÉER UN BOOLEAN À PARTIR D’UNE BOUNDING BOX #

Création objet de découpe à partir d’une bounding box

def make_bbox_cutter_from_extents(
    _xmin,
    _xmax,
    _ymin,
    _ymax,
    _zmin,
    _zmax,
    _offset,
    _cutter_name,
):
    _cx = (_xmin + _xmax) / 2
    _cy = (_ymin + _ymax) / 2
    _cz = (_zmin + _zmax) / 2

    _sx = max((_xmax - _xmin) * _offset, 0.001)
    _sy = max((_ymax - _ymin) * _offset, 0.001)
    _sz = max((_zmax - _zmin) * _offset, 0.001)

    _shift_x = random.uniform(
        -(_xmax - _xmin) * CUTTER_SHIFT_FACTOR,
        (_xmax - _xmin) * CUTTER_SHIFT_FACTOR,
    )
    _shift_y = random.uniform(
        -(_ymax - _ymin) * CUTTER_SHIFT_FACTOR,
        (_ymax - _ymin) * CUTTER_SHIFT_FACTOR,
    )
    _shift_z = random.uniform(
        -(_zmax - _zmin) * CUTTER_SHIFT_FACTOR,
        (_zmax - _zmin) * CUTTER_SHIFT_FACTOR,
    )

    _cx += _shift_x
    _cy += _shift_y
    _cz += _shift_z

    _hx = _sx / 2
    _hy = _sy / 2
    _hz = _sz / 2

    _mesh = bpy.data.meshes.new(_cutter_name + "_mesh")
    _obj = bpy.data.objects.new(_cutter_name, _mesh)
    bpy.context.collection.objects.link(_obj)

    _verts = [
        (_cx - _hx, _cy - _hy, _cz - _hz),
        (_cx - _hx, _cy - _hy, _cz + _hz),
        (_cx - _hx, _cy + _hy, _cz - _hz),
        (_cx - _hx, _cy + _hy, _cz + _hz),
        (_cx + _hx, _cy - _hy, _cz - _hz),
        (_cx + _hx, _cy - _hy, _cz + _hz),
        (_cx + _hx, _cy + _hy, _cz - _hz),
        (_cx + _hx, _cy + _hy, _cz + _hz),
    ]

    _faces = [
        (0, 4, 6, 2),
        (1, 3, 7, 5),
        (0, 1, 5, 4),
        (2, 6, 7, 3),
        (0, 2, 3, 1),
        (4, 5, 7, 6),
    ]

    _mesh.from_pydata(_verts, [], _faces)
    _mesh.update()

    _obj.hide_viewport = True
    _obj.hide_render = True

    return _obj
#
def make_bbox_cutter(_mesure_obj, _offset, _cutter_name):
    bpy.context.view_layer.update()
    _ex = get_extents(_mesure_obj)
    return make_bbox_cutter_from_extents(
        _ex[0],
        _ex[1],
        _ex[2],
        _ex[3],
        _ex[4],
        _ex[5],
        _offset,
        _cutter_name,
    )
#

APPLIQUER UN BOOLEAN SUR UNE BASSE #

Fonction pour appliquer une découpe booléenne sur un objet de bass

#
def apply_boolean_one(_bass, _cutter, _solver):
    bpy.ops.object.select_all(action="DESELECT")
    _bass.select_set(True)
    bpy.context.view_layer.objects.active = _bass

    _mod = _bass.modifiers.new(name="_bool_cut", type="BOOLEAN")
    _mod.operation = "DIFFERENCE"
    _mod.object = _cutter
    _mod.solver = _solver

    bpy.ops.object.modifier_apply(modifier=_mod.name)

    if _cutter.name in bpy.data.objects:
        bpy.data.objects.remove(_cutter, do_unlink=True)
#

APPLIQUER PLUSIEURS CUTTERS SUR LES BASSES #

Applique plusieurs découpes booléennes sur les objets de basses

#
def apply_all_booleans(_bass_targets, _mesure_objects, _offset, _solver, _max_count):
    _mesh_mesures = [o for o in _mesure_objects if o.type == "MESH"]
    if len(_mesh_mesures) == 0:
        return

    _filtered_mesures = []
    _processed_names = set()

    for _obj in _mesh_mesures:
        if _obj.name in _processed_names:
            continue

        if "Hihat" in _obj.name and _obj.name.endswith(".001"):
            continue

        _filtered_mesures.append(_obj)

        if "Hihat" in _obj.name and not _obj.name.endswith(".001"):
            _partner = bpy.data.objects.get(_obj.name + ".001")
            if _partner:
                _processed_names.add(_partner.name)

        _processed_names.add(_obj.name)

    if len(_filtered_mesures) == 0:
        return

    _count = min(_max_count, len(_filtered_mesures))
    _chosen_mesures = random.sample(_filtered_mesures, _count)

    for _mesure_obj in _chosen_mesures:
        if "Hihat" in _mesure_obj.name and not _mesure_obj.name.endswith(".001"):
            _partner = bpy.data.objects.get(_mesure_obj.name + ".001")

            if _partner and _partner.type == "MESH":
                _ex1 = get_extents(_mesure_obj)
                _ex2 = get_extents(_partner)

                _xmin = min(_ex1[0], _ex2[0])
                _xmax = max(_ex1[1], _ex2[1])
                _ymin = min(_ex1[2], _ex2[2])
                _ymax = max(_ex1[3], _ex2[3])
                _zmin = min(_ex1[4], _ex2[4])
                _zmax = max(_ex1[5], _ex2[5])

                _base_cutter = make_bbox_cutter_from_extents(
                    _xmin,
                    _xmax,
                    _ymin,
                    _ymax,
                    _zmin,
                    _zmax,
                    _offset,
                    "BOOL_" + _mesure_obj.name + "_pair",
                )
            else:
                _base_cutter = make_bbox_cutter(
                    _mesure_obj,
                    _offset,
                    "BOOL_" + _mesure_obj.name,
                )
        else:
            _base_cutter = make_bbox_cutter(
                _mesure_obj, _offset, "BOOL_" + _mesure_obj.name
            )

        for _bass in _bass_targets:
            if _bass.type != "MESH":
                continue

            _cutter_copy = _base_cutter.copy()
            _cutter_copy.data = _base_cutter.data.copy()
            _cutter_copy.name = _base_cutter.name + "_copy"
            _cutter_copy.data.name = _base_cutter.name + "_copy_mesh"
            bpy.context.collection.objects.link(_cutter_copy)
            _cutter_copy.hide_viewport = True
            _cutter_copy.hide_render = True

            apply_boolean_one(_bass, _cutter_copy, _solver)

        if _base_cutter.name in bpy.data.objects:
            bpy.data.objects.remove(_base_cutter, do_unlink=True)

    bpy.ops.object.select_all(action="DESELECT")
    for _obj in list(bpy.data.objects):
        if _obj.name.startswith("BOOL_"):
            bpy.data.objects.remove(_obj, do_unlink=True)
#

LANCER LES BOOLEANS SUR LES BASSES DU CUBE #

apply_all_booleans(
    _bass_targets=[o for o in _selected if o.type == "MESH"],
    _mesure_objects=_mesure_objects,
    _offset=BOOLEAN_OFFSET,
    _solver=BOOLEAN_SOLVER,
    _max_count=MAX_BOOLEAN_BOXES,
)
#

CRÉER LES DRUMS DU MORCEAU SCRIPT COMPLET SI ABSENT #

_drums_already_exist = any(
    o.name.startswith(("P1_", "P2_", "P3_", "P4_", "P5_", "P6_", "P7_", "P8_"))
    for o in bpy.data.objects
)

if not _drums_already_exist:
    part = 0
    for i in range(1, 17):
        beat_box(
            (ox(2 * i - 0.5, part), oy(0), 8), (0.125, 0.125, 0.125), f"P1_Kick{i}"
        )
        if i % 4 == 0:
            beat_triangle(
                (ox(2 * i - 0.5, part), oy(0), 6),
                (0.125, 0.02, 0.0625),
                f"P1_FX{i}",
            )

    part = 1
    for i in range(1, 17):
        beat_box(
            (ox(2 * i - 0.5, part), oy(0), 8), (0.125, 0.125, 0.125), f"P2_Kick{i}"
        )
        if i % 4 == 0:
            beat_triangle(
                (ox(2 * i - 0.5, part), oy(0), 6),
                (0.125, 0.02, 0.0625),
                f"P2_FX{i}",
            )

    beat_snare(
        (ox(30 + 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), "P2_Snare_extra1"
    )
    beat_snare(
        (ox(22 + 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), "P2_Snare_extra2"
    )

    for i in range(1, 17):
        beat_snare(
            (ox(2 * i - 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), f"P2_Snare{i}"
        )

    part = 2
    for i in range(1, 17):
        if i % 4 == 0:
            beat_triangle(
                (ox(2 * i - 0.5, part), oy(0), 6),
                (0.125, 0.02, 0.0625),
                f"P3_FX{i}",
            )

    beat_snare(
        (ox(30 + 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), "P3_Snare_extra1"
    )
    beat_snare(
        (ox(22 + 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), "P3_Snare_extra2"
    )

    for i in range(1, 17):
        beat_snare(
            (ox(2 * i - 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), f"P3_Snare{i}"
        )

    part = 3
    for i in range(1, 17):
        beat_box(
            (ox(2 * i - 0.5, part), oy(0), 8), (0.125, 0.125, 0.125), f"P4_Kick{i}"
        )
        if i % 4 == 0:
            beat_triangle(
                (ox(2 * i - 0.5, part), oy(0), 6),
                (0.125, 0.02, 0.0625),
                f"P4_FX{i}",
            )

    for i in range(1, 17):
        beat_hihat(
            (ox(2 * i - 0.5, part), oy(0), 4),
            (0.125 * 0.15, 0.125, 0.0625),
            f"P4_Hihat{i}",
        )

    for i in range(9, 17):
        beat_snare(
            (ox(2 * i - 0.5, part), oy(0), 0), (0.125, 0.125, 0.0625), f"P4_Snare{i}"
        )

    for i in range(4, 13):
        beat_triangle_box(
            (ox(2 * i, part), oy(-0.5), 2),
            (0.0625, 0.125, 0.0625),
            f"P4_Clap{i}",
        )

    part = 4
    for i in range(1, 17):
        if i % 4 == 0:
            beat_triangle(
                (ox(2 * i - 0.5, part), oy(0), 6),
                (0.125, 0.02, 0.0625),
                f"P5_FX{i}",
            )

    for i in range(1, 17):
        beat_hihat(
            (ox(2 * i - 0.5, part), oy(0), 4),
            (0.125 * 0.15, 0.125, 0.0625),
            f"P5_Hihat{i}",
        )

    for i in range(5, 17):
        beat_box(
            (ox(2 * i - 0.5, part), oy(0), 0), (0.125, 0.125, 0.0625), f"P5_Clap{i}"
        )

    for i in range(9, 17):
        beat_losange(
            (ox(2 * i, part), oy(-0.5), -2), (0.0625, 0.125, 0.0625), f"P5_Crash{i}"
        )

    part = 5
    for i in range(1, 17):
        beat_box(
            (ox(2 * i - 0.5, part), oy(0), 8), (0.125, 0.125, 0.125), f"P6_Kick{i}"
        )
        beat_hihat(
            (ox(2 * i - 0.5, part), oy(0), 4),
            (0.125 * 0.15, 0.125, 0.0625),
            f"P6_Hihat{i}",
        )
        beat_snare(
            (ox(2 * i - 0.5, part), oy(0), 0), (0.125, 0.125, 0.0625), f"P6_Snare{i}"
        )
        beat_triangle_box(
            (ox(2 * i, part), oy(-0.5), 2),
            (0.0625, 0.125, 0.0625),
            f"P6_Clap{i}",
        )
        beat_losange(
            (ox(2 * i, part), oy(-0.5), -2), (0.0625, 0.125, 0.0625), f"P6_Crash{i}"
        )
        if i % 4 == 0:
            beat_triangle(
                (ox(2 * i - 0.5, part), oy(0), 6),
                (0.125, 0.02, 0.0625),
                f"P6_FX{i}",
            )

    part = 6
    for i in range(1, 14):
        beat_box(
            (ox(2 * i - 0.5, part), oy(0), 8), (0.125, 0.125, 0.125), f"P7_Kick{i}"
        )

    for i in range(1, 17):
        if i % 4 == 0:
            beat_triangle(
                (ox(2 * i - 0.5, part), oy(0), 6),
                (0.125, 0.02, 0.0625),
                f"P7_FX{i}",
            )

    for i in range(1, 17):
        beat_hihat(
            (ox(2 * i - 0.5, part), oy(0), 4),
            (0.125 * 0.15, 0.125, 0.0625),
            f"P7_Hihat{i}",
        )

    for i in range(1, 12):
        beat_box(
            (ox(2 * i - 0.5, part), oy(0), 0), (0.125, 0.125, 0.125), f"P7_Clap{i}"
        )

    for i in range(1, 5):
        beat_triangle_box(
            (ox(2 * i, part), oy(-0.5), 2),
            (0.0625, 0.125, 0.0625),
            f"P7_Snare{i}",
        )

    for i in range(1, 8):
        beat_losange(
            (ox(2 * i, part), oy(-0.5), -2), (0.0625, 0.125, 0.0625), f"P7_Crash{i}"
        )

    part = 7
    beat_snare(
        (ox(30 + 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), "P8_Snare_extra1"
    )
    beat_snare(
        (ox(22 + 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), "P8_Snare_extra2"
    )

    for i in range(1, 17):
        beat_snare(
            (ox(2 * i - 0.5, part), oy(0), 4), (0.125, 0.125, 0.0625), f"P8_Snare{i}"
        )
#

SÉLECTION FINALE #

Sélectionne les mesures/ drums finales dans la scene

bpy.ops.object.select_all(action="DESELECT")

_prefix_front = f"P{_mesure_part_front + 1}_"
_prefix_left = f"P{_mesure_part_left + 1}_"
_prefix_right = f"P{_mesure_part_right + 1}_"
_prefix_back = f"P{_mesure_part_back + 1}_"

for _obj in bpy.data.objects:
    if (
        _obj.name.startswith(_prefix_front)
        or _obj.name.startswith(_prefix_left)
        or _obj.name.startswith(_prefix_right)
        or _obj.name.startswith(_prefix_back)
    ):
        _obj.select_set(True)
#

RÉSUMÉ FINAL #

os.system("cls")
print()
print("##################################")
print("#          RÉSUMÉ FINAL         #")
print("##################################")
print()
print("Basses empilées (copies temporaires) :")
for i, _obj in enumerate(_selected):
    print("  [" + str(i + 1) + "] " + _obj.name)
print()
print("Ordre : bas -> haut")
print("  " + " -> ".join([o.name for o in _selected]))
print()
print("Les restants sont placés à côté :")
print("  " + " | ".join([o.name for o in _not_selected]))
print()
print("Mesures :")
print("  Front  : Partie", _mesure_part_front + 1)
print("  Left   : Partie", _mesure_part_left + 1)
print("  Right  : Partie", _mesure_part_right + 1)
print("  Back   : Partie", _mesure_part_back + 1)
print("##################################")